#pragma once
#pragma warning( push )
#pragma warning( disable : 4702 )

#include <windows.h>
#include <gl\gl.h>
#include <gl\glu.h>
#include <Utilities\glext.h>
#include <string>
#include <vector>
#include "Texture.h"
#include "Parameters.h"
#include <Utilities\Extensions.h>
#include <Utilities\Exceptions.h>

#include <Core\IO\MeshIO.hh>
#include <Core\Mesh\Types\TriMesh_ArrayKernelT.hh>
#include <Core\Mesh\Types\PolyMesh_ArrayKernelT.hh>
#include <Tools\Subdivider\Uniform\CompositeLoopT.hh>

using namespace std;
using namespace OpenMesh;

typedef TriMesh_ArrayKernelT<OpenMesh::Subdivider::Uniform::CompositeTraits> TriangleMesh;
typedef Vec2d TexCoord2D;

class RadiosityMesh;
class FaceIter;

class ItemBufferMeshSubdivider : public Subdivider::Uniform::CompositeLoopT<TriangleMesh>
{
public:
	void apply_rules(void);
};

struct ItemBufferTraits : public DefaultTraits
{
	FaceTraits
	{
	public:
		FaceT(void)
		{
			_occlusionQuery = 0;
			_texCoords.resize(4, TexCoord2D(0, 0));
		}

		~FaceT(void)
		{
			if(_occlusionQuery != 0)
			{
				glDeleteQueries(1, &_occlusionQuery);
			}
		}

		void GenerateItemBufferOcclusionQuery(void)
		{
			glGenQueries(1, &_occlusionQuery);
		}

		void SetTexCoord(const int texCoordIndex, const TexCoord2D& texCoord)
		{
			_texCoords[texCoordIndex] = texCoord;
		}

		const TexCoord2D GetTexCoord(const int texCoordIndex) const
		{
			return _texCoords[texCoordIndex];
		}

		const GLuint GetItemBufferOcclusionQueryResult(void) const
		{
			if(glIsQuery(_occlusionQuery) == GL_FALSE)
			{
				return 0;
			}

			GLuint result;
			glGetQueryObjectuiv(_occlusionQuery, GL_QUERY_RESULT, &result);
			return result;
		}

		const GLuint GetItemBufferOcclusionQuery(void) const
		{
			return _occlusionQuery;
		}

	private:
		vector<TexCoord2D> _texCoords;
		GLuint _occlusionQuery;
	};
};

typedef PolyMesh_ArrayKernelT<ItemBufferTraits> ItemBufferMesh;

struct RadiosityTraits : public DefaultTraits
{
	FaceTraits
	{
	public:
		FaceT(void) 
		{
			_hasUnshotRadiosity = false;
			_hasUnshotImportance = false;
			_occlusionQuery = 0;

			_textures.clear();
		}

		~FaceT(void)
		{
			if(_occlusionQuery != 0)
			{
				glDeleteQueries(1, &_occlusionQuery);
			}
			_textures.erase(_textures.begin(), _textures.end());
		}

		void Create(const int textureCount)
		{
			for(int i = 0; i < textureCount; i++)
			{
				_textures.push_back(new Texture2DRW((RadiosityTexture) i, GL_RGB16F_ARB, GL_RGB, GL_FLOAT, _requestedTextureSize[Width], _requestedTextureSize[Height]));
			}
		}

		void GenerateUserViewOcclusionQuery(void)
		{
			glGenQueries(1, &_occlusionQuery);
		}

		Texture2DRW& GetRadiosityTextureVariableAccumulated(const RadiosityAlgorithm algorithm) const
		{
			return (algorithm == ImportanceDriven) ? GetRadiosityTextureVariable(Ij) : GetRadiosityTextureVariable(Bj);
		}

		Texture2DRW& GetRadiosityTextureVariableDelta(const RadiosityAlgorithm algorithm) const
		{
			return (algorithm == ImportanceDriven)? GetRadiosityTextureVariable(DeltaIj) : GetRadiosityTextureVariable(DeltaBj);
		}

		Texture2DRW& GetRadiosityTextureVariable(const RadiosityTexture texture) const
		{
			vector<Texture* const>::const_iterator iter = _textures.begin() + texture;
			return dynamic_cast<Texture2DRW&>(**iter);
		}

		const float GetFaceArea(void) const 
		{ 
			return _faceArea; 
		}

		void SetFaceArea(const float faceArea) 
		{
			_faceArea = faceArea; 
		}

		const Vec2i GetTextureSize(const RadiosityTexture texture) const 
		{ 
			return GetTextureSize(texture, 0);
		}

		const Vec2i GetTextureSize(const RadiosityTexture texture, const int level) const 
		{ 
			if((texture != DeltaBj) && (texture != DeltaIj) && (level != 0))
			{
				throw GraphicsException("Can't get the size of a texture that doesn't have mips.");
			}

			return GetRadiosityTextureVariable(texture).GetTextureSize(level); 
		}

		const Vec2i GetTextureSize(const RadiosityAlgorithm algorithm) const 
		{ 
			return GetTextureSize(algorithm, 0);
		}

		const Vec2i GetTextureSize(const RadiosityAlgorithm algorithm, const int level) const 
		{ 
			return (algorithm == ImportanceDriven) ? GetTextureSize(DeltaIj, level) : GetTextureSize(DeltaBj, level);
		}

		void SetRequestedTextureSize(const Vec2i& size) 
		{
			_requestedTextureSize = size; 
		}

		const Vec3f GetFaceReflectivity(void) const 
		{ 
			return _faceReflectivity; 
		}

		void SetFaceReflectivity(const TriangleMesh::Color color) 
		{ 
			_faceReflectivity = Vec3f(color)/255.0; 
		}

		const bool GetHasUnshotRadiosity(const RadiosityAlgorithm algorithm) const
		{
			return (algorithm == ImportanceDriven) ? _hasUnshotImportance : _hasUnshotRadiosity;
		}

		void SetHasUnshotRadiosity(const bool hasUnshotRadiosity, const RadiosityAlgorithm algorithm)
		{
			if(algorithm == ImportanceDriven)
			{
				_hasUnshotImportance = hasUnshotRadiosity;
			}
			else
			{
				_hasUnshotRadiosity = hasUnshotRadiosity;
			}

			SetHasDirtyMips();
		}

		const bool GetHasDirtyMips(void) const
		{
			return _hasDirtyMips;
		}

		void SetHasDirtyMips(void)
		{
			SetHasDirtyMips(true);
		}

		void SetHasDirtyMips(const bool hasDirtyMips)
		{
			_hasDirtyMips = hasDirtyMips;
		}

		void AddChildFaceHandle(const ItemBufferMesh::FaceHandle face)
		{
			_childFaceHandles.push_back(face);
		}

		const vector<ItemBufferMesh::FaceHandle>& GetChildFaceHandles(void) const
		{
			return _childFaceHandles;
		}

		const bool GetIsVisibleHemisphere(const ItemBufferMesh& _itemBufferMesh) const
		{
			for (auto iter = _childFaceHandles.begin(); iter != _childFaceHandles.end(); ++iter)
			{
				ItemBufferMesh::ConstFaceIter faceIter(_itemBufferMesh, *iter);

				//Check to see if any sub-faces were visible 
				return (faceIter->GetItemBufferOcclusionQueryResult() > 0);
			}
			return false;
		}

		const bool GetIsVisibleUserView(void) const
		{
			return (GetUserViewOcclusionQueryResult() > 0);
		}

		vector<Vec4f>& GetUnshotShootPositions(void)
		{
			return _unshotShootPositions;
		}

		const GLuint GetUserViewOcclusionQueryResult(void) const
		{
			GLuint result;
			glGetQueryObjectuiv(_occlusionQuery, GL_QUERY_RESULT, &result);
			return result;
		}

		const GLuint GetUserViewOcclusionQuery(void) const
		{
			return _occlusionQuery;
		}

	private:
		vector<Texture* const> _textures;
		vector<ItemBufferMesh::FaceHandle> _childFaceHandles;

		Vec2i _requestedTextureSize;
		float _faceArea;
		Vec3f _faceReflectivity;
		bool _hasUnshotRadiosity;
		bool _hasUnshotImportance;
		bool _hasDirtyMips;
		vector<Vec4f> _unshotShootPositions;

		GLuint _occlusionQuery;
	};
};

class RadiosityMesh : public PolyMesh_ArrayKernelT<RadiosityTraits>
{
public:
	RadiosityMesh(void) {}
	virtual ~RadiosityMesh(void)
	{
		delete[] _fovOcclusionQueries;
	}

	void Create(const Parameters* const parameters);
	const FaceHandle GetFaceHandle(const GLint faceId) const;

	const int GetMaxTextureSize(const int dimension) const;
	const float GetModelSpanDistance(void) const;

	const ItemBufferMesh& GetItemBufferMesh(void) const;
	const TextureArray& GetFacePositionsTextureVariable(void) const;
	void GenerateFovOcclusionQueries(void);
	void RadiosityMesh::GetFovOcclusionQueryResults(GLubyte* results);
	const GLuint RadiosityMesh::GetFovOcclusionQuery(RadiosityMesh::FaceIter iter);

	const bool GetIsVisibleFov(RadiosityMesh::FaceIter faceIter) const;
private:
	RadiosityMesh(const RadiosityMesh&);

	void CopyTriangleColors(TriangleMesh& triangleMesh);
	void CreateRadiosityMesh(TriangleMesh& triangleMesh);
	void SetRadiosityMeshProperties(TriangleMesh& triangleMesh, FPropHandleT<float>& tempId);
	void CreateItemBufferMesh(TriangleMesh& triangleMesh, const int subdivisionLevel);
	void SetItemBufferMeshProperties(void);
	void FinalizeMeshes(void);

	Vec2i _maxTextureSize;
	GLfloat _modelSpanDistance;
	ItemBufferMesh _itemBufferMesh;
	TextureArray _facePositions;
	GLuint* _fovOcclusionQueries;

	template<typename Mesh>
	void IdentifyLongestHalfedge(Mesh& mesh, TriangleMesh& triangleMesh)
	{
		//Copy tri mesh into a poly mesh.
		mesh.assign(triangleMesh);

		//Go through each face
		for (Mesh::FaceIter faceIter = mesh.faces_begin(); faceIter != mesh.faces_end(); ++faceIter)
		{
			if(!mesh.status(faceIter.handle()).tagged())
			{
				Mesh::Scalar longest = 0;
				Mesh::HalfedgeHandle halfEdge;
				for (Mesh::FaceHalfedgeIter faceHalfedgeIter = mesh.fh_iter(faceIter.handle()); faceHalfedgeIter; ++faceHalfedgeIter)
				{
					//find the longest halfedge
					Mesh::Scalar length = mesh.calc_edge_length(faceHalfedgeIter.handle());
					if(length > longest)
					{
						longest = length;
						halfEdge = faceHalfedgeIter.handle();
					}
				}
				//tag it and the opposite halfedge and the two incident faces
				mesh.status(halfEdge).set_tagged(true);
				mesh.status(faceIter.handle()).set_tagged(true);
				mesh.status(mesh.opposite_halfedge_handle(halfEdge)).set_tagged(true);
				mesh.status(mesh.face_handle(mesh.opposite_halfedge_handle(halfEdge))).set_tagged(true);
			}
		}
	}

	template<typename Mesh>
	void ConvertTriangleMeshToPolyMesh(Mesh& mesh, TriangleMesh& triangleMesh, FPropHandleT<float>& tempId)
	{
		mesh.add_property(tempId);

		//Go back through face list
		TriangleMesh::FaceIter triangleFaceIter = triangleMesh.faces_begin();
		for (Mesh::FaceIter faceIter = mesh.faces_begin(); faceIter != mesh.faces_end(); ++faceIter, ++triangleFaceIter)
		{
			//for each face tagged, 
			if(mesh.status(faceIter.handle()).tagged())
			{
				TriangleMesh::FaceHalfedgeIter triangleFaceHalfedgeIter = triangleMesh.fh_iter(triangleFaceIter.handle());
				for (Mesh::FaceHalfedgeIter faceHalfedgeIter = mesh.fh_iter(faceIter.handle()); faceHalfedgeIter; ++faceHalfedgeIter, ++triangleFaceHalfedgeIter)
				{
					//find tagged halfedge
					if(mesh.status(faceHalfedgeIter.handle()).tagged())
					{
						//Old things to delete
						Mesh::HalfedgeHandle oldHalfedge1 = faceHalfedgeIter.handle();
						Mesh::HalfedgeHandle oldHalfedge2 = mesh.opposite_halfedge_handle(faceHalfedgeIter.handle());
						Mesh::EdgeHandle oldEdge = mesh.edge_handle(oldHalfedge1);
						Mesh::FaceHandle oldFace1 = faceIter.handle();
						Mesh::FaceHandle oldFace2 = mesh.face_handle(oldHalfedge2);
						//Parts of the original trimesh to update with the new face's handle Id
						TriangleMesh::HalfedgeHandle tempHalfEdge = triangleMesh.opposite_halfedge_handle(triangleFaceHalfedgeIter.handle());
						TriangleMesh::FaceHandle triangleOldFace1 = triangleFaceIter.handle();
						TriangleMesh::FaceHandle triangleOldFace2 = triangleMesh.face_handle(tempHalfEdge);

						//create a new face
						Mesh::FaceHandle newFace = mesh.new_face();
						//Copy the color and normal values
						mesh.set_color(newFace, mesh.color(oldFace1));
						mesh.set_normal(newFace, mesh.normal(oldFace1));
						//Copy in the new handle Id
						triangleMesh.set_normal(triangleOldFace1, TriangleMesh::Normal(static_cast<float>(newFace.idx()), 0, 0));
						triangleMesh.set_normal(triangleOldFace2, TriangleMesh::Normal(static_cast<float>(newFace.idx()), 0, 0));
						//Store the old id number, because it's going to change when garbage is collected
						Mesh::FaceIter tempIter(mesh, newFace);
						mesh.property(tempId, tempIter) = static_cast<float>(newFace.idx());

						//Halfedges to connect
						Mesh::HalfedgeHandle halfedge1 = mesh.next_halfedge_handle(oldHalfedge1);
						Mesh::HalfedgeHandle halfedge2 = mesh.next_halfedge_handle(halfedge1);
						Mesh::HalfedgeHandle halfedge3 = mesh.next_halfedge_handle(oldHalfedge2);
						Mesh::HalfedgeHandle halfedge4 = mesh.next_halfedge_handle(halfedge3);

						//update old face's halfedges and opposite halfedge face's halfedges to be connected
						mesh.set_next_halfedge_handle(halfedge2, halfedge3);
						mesh.set_next_halfedge_handle(halfedge4, halfedge1);

						//set all halfedges' face handles to new face
						mesh.set_face_handle(halfedge1, newFace);
						mesh.set_face_handle(halfedge2, newFace);
						mesh.set_face_handle(halfedge3, newFace);
						mesh.set_face_handle(halfedge4, newFace);

						//connect new face to a halfedge
						mesh.set_halfedge_handle(newFace, halfedge1);

						//set tagged halfedges', tagged faces', and tagged halfedges' edge status to delete
						mesh.status(oldHalfedge1).set_deleted(true);
						mesh.status(oldHalfedge2).set_deleted(true);
						mesh.status(oldFace1).set_deleted(true);
						mesh.status(oldFace2).set_deleted(true);
						mesh.status(oldEdge).set_deleted(true);

						//set both faces' tags to false
						mesh.status(oldFace1).set_tagged(false);
						mesh.status(oldFace2).set_tagged(false);

						break;
					}
				}
			}
		}

		//Run garbage collection
		mesh.garbage_collection();
	}
};

#pragma warning( pop )
